package cloud.seri.auth.service

import cloud.seri.auth.config.Constants
import cloud.seri.auth.domain.Authority
import cloud.seri.auth.domain.User
import cloud.seri.auth.repository.AuthorityRepository
import cloud.seri.auth.repository.RoleRepository
import cloud.seri.auth.repository.UserRepository
import cloud.seri.auth.security.AuthoritiesConstants
import cloud.seri.auth.security.SecurityUtils
import cloud.seri.auth.service.dto.UserDTO
import cloud.seri.auth.service.mapper.UserMapper
import cloud.seri.auth.service.util.RandomUtil
import cloud.seri.auth.web.rest.errors.EmailAlreadyUsedException
import cloud.seri.auth.web.rest.errors.InvalidPasswordException
import cloud.seri.auth.web.rest.errors.LoginAlreadyUsedException
import org.slf4j.LoggerFactory
import org.springframework.cache.CacheManager
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.mongodb.datatables.DataTablesInput
import org.springframework.data.mongodb.datatables.DataTablesOutput
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*

/**
 * Service class for managing users.
 */
@Service
class UserService(
    private val userRepository: UserRepository,
    private val passwordEncoder: PasswordEncoder,
    private val roleRepository: RoleRepository,
    private val cacheManager: CacheManager,
    private val userMapper: UserMapper
)
{

    private val log = LoggerFactory.getLogger(UserService::class.java)

    fun activateRegistration(key: String): Optional<User>
    {
        log.debug("Activating user for activation key {}", key)
        return userRepository.findOneByActivationKey(key)
            .map { user ->
                // activate given user for the registration key.
                user.activated = true
                user.activationKey = null
                userRepository.save(user)
                clearUserCaches(user)
                log.debug("Activated user: {}", user)
                user
            }
    }

    fun completePasswordReset(newPassword: String, key: String): Optional<User>
    {
        log.debug("Reset user password for reset key {}", key)
        return userRepository.findOneByResetKey(key)
            .filter { user -> user.resetDate?.isAfter(Instant.now().minusSeconds(86400)) ?: false }
            .map { user ->
                user.password = passwordEncoder.encode(newPassword)
                user.resetKey = null
                user.resetDate = null
                userRepository.save(user)
                clearUserCaches(user)
                user
            }
    }

    fun requestPasswordReset(mail: String): Optional<User>
    {
        return userRepository.findOneByEmailIgnoreCase(mail)
            .filter(User::activated)
            .map { user ->
                user.resetKey = RandomUtil.generateResetKey()
                user.resetDate = Instant.now()
                userRepository.save(user)
                clearUserCaches(user)
                user
            }
    }

    fun registerUser(userDTO: UserDTO, password: String): User
    {
        val login = userDTO.login ?: throw IllegalArgumentException("Empty login not allowed")
        val email = userDTO.email ?: throw IllegalArgumentException("Empty email not allowed")
        userRepository.findOneByLogin(login.toLowerCase()).ifPresent { existingUser ->
            val removed = removeNonActivatedUser(existingUser)
            if (!removed)
            {
                throw LoginAlreadyUsedException()
            }
        }
        userRepository.findOneByEmailIgnoreCase(email).ifPresent { existingUser ->
            val removed = removeNonActivatedUser(existingUser)
            if (!removed)
            {
                throw EmailAlreadyUsedException()
            }
        }
        val newUser = userMapper.toEntity(userDTO)
        newUser.password = passwordEncoder.encode(password)
        newUser.login = login.toLowerCase()
        newUser.email = email.toLowerCase()
        newUser.activated = false
        newUser.activationKey = RandomUtil.generateActivationKey()
        userRepository.save(newUser)
        clearUserCaches(newUser)
        log.debug("Created Information for User: {}", newUser)
        return newUser
    }

    private fun removeNonActivatedUser(existingUser: User): Boolean
    {
        if (existingUser.activated)
        {
            return false
        }
        userRepository.delete(existingUser)
        clearUserCaches(existingUser)
        return true
    }

    fun createUser(userDTO: UserDTO): UserDTO
    {
        val user = userMapper.toEntity(userDTO)
        user.login = userDTO.login?.toLowerCase()
        user.email = userDTO.email?.toLowerCase()
        user.imageUrl = userDTO.imageUrl
        if (userDTO.langKey == null)
        {
            user.langKey = Constants.DEFAULT_LANGUAGE // default language
        }
        else
        {
            user.langKey = userDTO.langKey
        }
        val encryptedPassword = passwordEncoder.encode(RandomUtil.generatePassword())
        user.password = encryptedPassword
        user.resetKey = RandomUtil.generateResetKey()
        user.resetDate = Instant.now()
        user.activated = true
        userDTO.roles?.let {
            user.roles = roleRepository.findAllById(it.asIterable()).toMutableSet()
        }

        userRepository.save(user)
        clearUserCaches(user)
        log.debug("Created Information for User: {}", user)
        return userMapper.toDto(user)
    }

    /**
     * Update basic information (first name, last name, email, language) for the current user.
     *
     * @param firstName first name of user.
     * @param lastName last name of user.
     * @param email email id of user.
     * @param langKey language key.
     * @param imageUrl image URL of user.
     */
    fun updateUser(userName: String?, realName: String?, email: String?, langKey: String?, imageUrl: String?)
    {
        SecurityUtils.getCurrentUserLogin()
            .flatMap(userRepository::findOneByLogin)
            .ifPresent { user ->
                user.userName = userName
                user.realName = realName
                user.email = email?.toLowerCase()
                user.langKey = langKey
                user.imageUrl = imageUrl
                userRepository.save(user)
                clearUserCaches(user)
                log.debug("Changed Information for User: {}", user)
            }
    }

    /**
     * Update all information for a specific user, and return the modified user.
     *
     * @param userDTO user to update.
     * @return updated user.
     */
    fun updateUser(userDTO: UserDTO): Optional<UserDTO>
    {
        val u = userMapper.toEntity(userDTO)
        return Optional.of(userRepository.findById(userDTO.id!!))
            .filter(Optional<User>::isPresent)
            .map { it.get() }
            .map { user ->
                clearUserCaches(user)
                user.login = userDTO.login!!.toLowerCase()
                user.userName = userDTO.userName
                user.realName = userDTO.realName
                user.email = userDTO.email?.toLowerCase()
                user.imageUrl = userDTO.imageUrl
                user.activated = userDTO.activated
                user.langKey = userDTO.langKey
                user.status = userDTO.status
                userDTO.roles?.let {
                    user.roles = roleRepository.findAllById(it).toMutableSet()
                }
                user.dept = u.dept
                user.phone = u.phone
                user.sex = u.sex
                user.birthday = u.birthday
                userRepository.save(user)
                this.clearUserCaches(user)
                log.debug("Changed Information for User: {}", user)
                user
            }
            .map { userMapper.toDto(it) }
    }

    fun deleteUser(login: String)
    {
        userRepository.findOneByLogin(login).ifPresent { user ->
            userRepository.delete(user)
            clearUserCaches(user)
            log.debug("Deleted User: {}", user)
        }
    }

    fun changePassword(currentClearTextPassword: String, newPassword: String)
    {
        SecurityUtils.getCurrentUserLogin()
            .flatMap(userRepository::findOneByLogin)
            .ifPresent { user ->
                val currentEncryptedPassword = user.password
                if (!passwordEncoder.matches(currentClearTextPassword, currentEncryptedPassword))
                {
                    throw InvalidPasswordException()
                }
                val encryptedPassword = passwordEncoder.encode(newPassword)
                user.password = encryptedPassword
                userRepository.save(user)
                clearUserCaches(user)
                log.debug("Changed password for User: {}", user)
            }
    }

    fun getAllManagedUsers(pageable: Pageable): Page<UserDTO>
    {
        return userRepository.findAllByLoginNot(pageable, Constants.ANONYMOUS_USER).map { userMapper.toDto(it) }
    }

    fun getUserWithAuthoritiesByLogin(login: String): Optional<User>
    {
        return userRepository.findOneByLogin(login)
    }

    fun getUserWithAuthorities(id: String): Optional<User>
    {
        return userRepository.findById(id)
    }

    fun getUserWithAuthorities(): Optional<User>
    {
        return SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneByLogin)
    }

    /**
     * Not activated users should be automatically deleted after 3 days.
     *
     * This is scheduled to get fired everyday, at 01:00 (am).
     */
    @Scheduled(cron = "0 0 1 * * ?")
    fun removeNotActivatedUsers()
    {
        userRepository
            .findAllByActivatedIsFalseAndCreatedDateBefore(Instant.now().minus(3, ChronoUnit.DAYS))
            .forEach { user ->
                log.debug("Deleting not activated user {}", user.login)
                userRepository.delete(user)
                clearUserCaches(user)
            }
    }

    /**
     * @return a list of all the authorities
     */
    fun getAuthorities(): MutableList<String>
    {
        return roleRepository.findAll().asSequence().map { it.name }.filterNotNullTo(mutableListOf())
    }

    private fun clearUserCaches(user: User)
    {
        cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE)?.evict(user.login!!)
        cacheManager.getCache(UserRepository.USERS_BY_EMAIL_CACHE)?.evict(user.email!!)
    }

    fun getUsers(input: DataTablesInput): DataTablesOutput<UserDTO>
    {
        val result = userRepository.findAll(input)
        return DataTablesOutput<UserDTO>().apply {
            draw = result.draw
            recordsTotal = result.recordsTotal
            recordsFiltered = result.recordsFiltered
            data = result.data.map {
                userMapper.toDto(it).apply {
                    roles = it.roles.map { it.id!! }
                }
            }
            error = result.error
        }
    }
}
